using System;
using System.IO;
using System.CodeDom;
using System.CodeDom.Compiler;
using System.Text.RegularExpressions;
using System.Windows.Forms;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;

namespace EffectGenerator
{
    [System.Runtime.InteropServices.ComVisible(true)]
    [System.Runtime.InteropServices.Guid("DC8106CE-3A1F-11DB-8AF6-B622A1EF5492")]
    [Microsoft.VisualStudio.Shell.CodeGeneratorRegistration(typeof(EffectGenerator), "C# XNA Effect Generator", VSLangProj80.vsContextGuids.vsContextGuidVCSProject, GeneratesDesignTimeSource = true)]
    [Microsoft.VisualStudio.Shell.ProvideObject(typeof(EffectGenerator))]
    public class EffectGenerator : Microsoft.Samples.VisualStudio.GeneratorSample.BaseCodeGeneratorWithSite
	{
        public const uint LineOrColumnNull = uint.MaxValue;

        private static bool IsArray(EffectParameter parameter)
        {
            return parameter.Elements.Count != 0;
        }

        private static Type GetParameterType(EffectParameter effectParameter)
        {
            bool isArray = IsArray(effectParameter);

            switch (effectParameter.ParameterClass)
            {
                case EffectParameterClass.MatrixColumns:
                    throw new NotSupportedException(Strings.UseRowMatrixInstead);

                case EffectParameterClass.MatrixRows:
                    if (effectParameter.ParameterType == EffectParameterType.Single && effectParameter.RowCount == 4 && effectParameter.ColumnCount == 4)
                        return typeof(Matrix);
                    break;

                case EffectParameterClass.Object:

                    switch (effectParameter.ParameterType)
                    {
                        case EffectParameterType.String:
                            return typeof(String);

                        case EffectParameterType.Texture:
                            throw new NotSupportedException(Strings.UseTextureSubclassInstead);

                        case EffectParameterType.Texture2D:
                            if (isArray)
                                throw new NotSupportedException(Strings.ArraysNotSupported);
                            return typeof(Texture2D);

                        case EffectParameterType.Texture3D:
                            if (isArray)
                                throw new NotSupportedException(Strings.ArraysNotSupported);
                            return typeof(Texture3D);

                        case EffectParameterType.TextureCube:
                            if (isArray)
                                throw new NotSupportedException(Strings.ArraysNotSupported);
                            return typeof(TextureCube);
                    }
                    break;

                case EffectParameterClass.Scalar:
                    switch (effectParameter.ParameterType)
                    {
                        case EffectParameterType.Single:
                            return typeof(Single);
                    }
                    break;

                case EffectParameterClass.Struct:
                    throw new NotSupportedException(""); //TODO: support structs

                case EffectParameterClass.Vector:

                    switch (effectParameter.ParameterType)
                    {
                        case EffectParameterType.Single:
                            switch (effectParameter.ColumnCount)
                            {
                                case 2:
                                    return typeof(Vector2);
                                case 3:
                                    return typeof(Vector3);
                                case 4:
                                    return typeof(Vector4);
                            }
                            break;
                    }
                    break;
            }
            
            throw new NotSupportedException("");
        }

        protected void ReportProgress(uint complete, uint total)
        {
            if (this.CodeGeneratorProgress != null)
                this.CodeGeneratorProgress.Progress(complete, total);
        }

        public string GenerateCodeForFile()
        {
            #region Create graphics device

            PresentationParameters parameters = new PresentationParameters();
            parameters.BackBufferWidth = 1;
            parameters.BackBufferHeight = 1;
            Form dummy = new Form();
            
            GraphicsDevice device = new GraphicsDevice(GraphicsAdapter.DefaultAdapter, DeviceType.NullReference, dummy.Handle, CreateOptions.SoftwareVertexProcessing, parameters);

            #endregion

            #region Create effect

            CompiledEffect windowsCompiledEffect = Effect.CompileEffectFromFile(this.InputFilePath, null, null, CompilerOptions.None, TargetPlatform.Windows);
            CompiledEffect xbox360CompiledEffect = Effect.CompileEffectFromFile(this.InputFilePath, null, null, CompilerOptions.None, TargetPlatform.Xbox360);

            if (!string.IsNullOrEmpty(windowsCompiledEffect.ErrorsAndWarnings))
            {
				bool hasErrors = false;

                Regex findLineNumberRegEx = new Regex(@"\.fx\((?<line>\d+)\): ");
                foreach (string errorOrWarning in windowsCompiledEffect.ErrorsAndWarnings.Split(new char[] { '\n' }))
                {
                    if (string.IsNullOrEmpty(errorOrWarning))
                        continue;

                    Match match = findLineNumberRegEx.Match(errorOrWarning);
                    if (match.Success)
                    {
                        Group lineNumberGroup = match.Groups["line"];
                        uint lineNumber = uint.Parse(lineNumberGroup.Value);
                        string errorOrWarningStripped = errorOrWarning.Substring(match.Index + match.Length);

						const string warningPrefix = "warning ";
						if (errorOrWarningStripped.StartsWith(warningPrefix))
						{
							this.GeneratorWarning(4, errorOrWarningStripped.Substring(warningPrefix.Length), lineNumber - 1, LineOrColumnNull);
						}
						else
						{
							hasErrors = true;
							this.GeneratorError(4, errorOrWarningStripped, lineNumber - 1, LineOrColumnNull);
						}
                    }
                    else
                    {
                        this.GeneratorError(4, errorOrWarning, LineOrColumnNull, LineOrColumnNull);
						hasErrors = true;
                    }
                }

				if (hasErrors)
					throw new GenerationFailedException();
            }

            byte[] windowsEffectCode = windowsCompiledEffect.GetEffectCode();
            byte[] xbox360EffectCode = xbox360CompiledEffect.GetEffectCode();

            // Use the windows code during generation time
            Effect effect = new Effect(device, windowsEffectCode, CompilerOptions.None, null);
            
            #endregion

            this.ReportProgress(1, 3);

            #region Generate code

            // Create some shared code dom objects
			CodePrimitiveExpression nullExpression = new CodePrimitiveExpression(null);
            CodeThisReferenceExpression codeThisReferenceExpression = new CodeThisReferenceExpression();
            CodePropertySetValueReferenceExpression codePropertySetValueReferenceExpression = new CodePropertySetValueReferenceExpression();

            CodeTypeReference effectParameterTypeReference = new CodeTypeReference(typeof(EffectParameter));           
            CodeMethodReferenceExpression parametersCollectionReference = new CodeMethodReferenceExpression(codeThisReferenceExpression, "Parameters");


//TODO: generate header (generated by, "do not modify", generaton date/time)
            // Create namespace
            CodeNamespace codeNamespace = new CodeNamespace(this.FileNameSpace);
            codeNamespace.Imports.Add(new CodeNamespaceImport("Microsoft.Xna.Framework"));
            codeNamespace.Imports.Add(new CodeNamespaceImport("Microsoft.Xna.Framework.Graphics"));
            //codeNamespace.Imports.Add(new CodeNamespaceImport("EffectGenerator"));
            
            // Create effect type
            CodeTypeDeclaration codeTypeDeclaration = new CodeTypeDeclaration(Path.GetFileNameWithoutExtension(this.InputFilePath));
            codeTypeDeclaration.BaseTypes.Add(typeof(Microsoft.Xna.Framework.Graphics.Effect));
            codeNamespace.Types.Add(codeTypeDeclaration);

            // Create static shader code field
            const string EffectCodeFieldName = "effectCode";
            CodeMemberField effectCodeField = new CodeMemberField(typeof(byte[]), EffectCodeFieldName);
            effectCodeField.Attributes = MemberAttributes.Private | MemberAttributes.Static;
            codeTypeDeclaration.Members.Add(effectCodeField);
            CodeVariableReferenceExpression effectCodeVariableReferenceExpression = new CodeVariableReferenceExpression(EffectCodeFieldName);

            // Create effect code initalizers
            CodeExpression[] windowsEffectCodeInitalizers = new CodeExpression[windowsEffectCode.Length];
            for (int i = 0; i < windowsEffectCodeInitalizers.Length; i++)
                windowsEffectCodeInitalizers[i] = new CodePrimitiveExpression(windowsEffectCode[i]);
            CodeExpression[] xbox360EffectCodeInitalizers = new CodeExpression[xbox360EffectCode.Length];
            for (int i = 0; i < xbox360EffectCodeInitalizers.Length; i++)
                xbox360EffectCodeInitalizers[i] = new CodePrimitiveExpression(xbox360EffectCode[i]);
            
            // Create static constructor to initalize shader code field
            //TODO: Less hacky way to select between 360 and Windows
            CodeTypeConstructor staticConstructor = new CodeTypeConstructor();
            staticConstructor.Statements.Add(new CodeSnippetExpression("#if XBOX360 //"));
                staticConstructor.Statements.Add(
                    new CodeAssignStatement(
                        effectCodeVariableReferenceExpression,
                        new CodeArrayCreateExpression(new CodeTypeReference(typeof(byte[])), xbox360EffectCodeInitalizers)));
            staticConstructor.Statements.Add(new CodeSnippetExpression("#else //"));
                staticConstructor.Statements.Add(
                    new CodeAssignStatement(
                        effectCodeVariableReferenceExpression,
                        new CodeArrayCreateExpression(new CodeTypeReference(typeof(byte[])), windowsEffectCodeInitalizers)));
            staticConstructor.Statements.Add(new CodeSnippetExpression("#endif //"));
            codeTypeDeclaration.Members.Add(staticConstructor);

            // Create constructor
            CodeConstructor constructor = new CodeConstructor();
            constructor.Attributes = MemberAttributes.Public;
            const string graphicsDeviceParameterName = "graphicsDevice";
            const string effectPoolParameterName = "effectPool";
			CodeParameterDeclarationExpression graphicsDeviceParameter = new CodeParameterDeclarationExpression(typeof(GraphicsDevice), graphicsDeviceParameterName);
            constructor.Parameters.Add(graphicsDeviceParameter);
            constructor.Parameters.Add(new CodeParameterDeclarationExpression(typeof(EffectPool), effectPoolParameterName));
			CodeVariableReferenceExpression graphicsDeviceParameterReference = new CodeVariableReferenceExpression(graphicsDeviceParameterName);
            constructor.BaseConstructorArgs.Add(graphicsDeviceParameterReference);
            constructor.BaseConstructorArgs.Add(effectCodeVariableReferenceExpression);
            constructor.BaseConstructorArgs.Add(new CodeFieldReferenceExpression(new CodeTypeReferenceExpression(typeof(CompilerOptions)), "None"));
            constructor.BaseConstructorArgs.Add(new CodeVariableReferenceExpression(effectPoolParameterName));
            codeTypeDeclaration.Members.Add(constructor);
            // Note: constructor statements are generated during the effect parameter loop

			// Create simplified constructor (defaults effectPool = null)
			CodeConstructor simplifiedConstructor = new CodeConstructor();
			simplifiedConstructor.Attributes = MemberAttributes.Public;
			simplifiedConstructor.Parameters.Add(graphicsDeviceParameter);
			simplifiedConstructor.ChainedConstructorArgs.Add(graphicsDeviceParameterReference);
			simplifiedConstructor.ChainedConstructorArgs.Add(nullExpression);
			codeTypeDeclaration.Members.Add(simplifiedConstructor);

            // Create a field and property for each effect parameter
            for (int i = 0; i < effect.Parameters.Count; i++)
            {
                EffectParameter parameter = effect.Parameters[i];
                Type parameterType;
                try
                {
                    parameterType = GetParameterType(parameter);
                }
                catch (NotSupportedException notSupportedException)
                {
                    // Unable to create a property for this paramter
                    this.GeneratorWarning(4, string.Format(Strings.NotSupportedEffectParameter, parameter.Name, notSupportedException.Message), LineOrColumnNull, LineOrColumnNull);
                    continue;
                }

                bool isArray = IsArray(parameter);

                CodeTypeReference propertyTypeReference;
                if (isArray)
                {
                    propertyTypeReference = new CodeTypeReference(parameterType.Name + "ParameterCollection");
                }
                else
                {
                    propertyTypeReference = new CodeTypeReference(parameterType);
                }

                // Create field
                string fieldName = "_" + parameter.Name;
                CodeMemberField memberField = new CodeMemberField(
                    isArray ? propertyTypeReference : effectParameterTypeReference,
                    fieldName);
                codeTypeDeclaration.Members.Add(memberField);
                CodeFieldReferenceExpression fieldReference = new CodeFieldReferenceExpression(codeThisReferenceExpression, fieldName);

                // Create property
                CodeMemberProperty memberProperty = new CodeMemberProperty();
                memberProperty.Type = propertyTypeReference;
                memberProperty.Name = parameter.Name;
                memberProperty.Attributes = MemberAttributes.Public | MemberAttributes.Final;
                codeTypeDeclaration.Members.Add(memberProperty);

                // Attach summary doc comments to the property if the parameter has the annotation
                EffectAnnotation sasUiDescriptionAnnotation = parameter.Annotations["SasUiDescription"];
                if (sasUiDescriptionAnnotation != null)
                {
                    memberProperty.Comments.Add(new CodeCommentStatement("<summary>", true));
                    memberProperty.Comments.Add(new CodeCommentStatement(sasUiDescriptionAnnotation.GetValueString(), true));
                    memberProperty.Comments.Add(new CodeCommentStatement("</summary>", true));
                }

                // Create property get
                memberProperty.HasGet = true;
                if (isArray)
                {
                    memberProperty.GetStatements.Add(new CodeMethodReturnStatement(fieldReference));
                }
                else
                {
                    memberProperty.GetStatements.Add(
                        new CodeMethodReturnStatement(
                            new CodeMethodInvokeExpression(
                                new CodeMethodReferenceExpression(
                                    fieldReference,
                                    "GetValue" + parameterType.Name))));
                }

                if (!isArray)
                {
                    // Create property set`
                    memberProperty.HasSet = true;
                    memberProperty.SetStatements.Add(
                        new CodeMethodInvokeExpression(fieldReference, "SetValue", codePropertySetValueReferenceExpression));
                }

                // Create statement for this field in the constructor
                CodeArrayIndexerExpression parameterIndexerExpression = new CodeArrayIndexerExpression(parametersCollectionReference, new CodePrimitiveExpression(parameter.Name));
                if (isArray)
                {
                    constructor.Statements.Add(
                        new CodeAssignStatement(
                            fieldReference,
                            new CodeObjectCreateExpression(propertyTypeReference, new CodePropertyReferenceExpression(parameterIndexerExpression, "Elements"))));
                }
                else
                {
                    constructor.Statements.Add(
                        new CodeAssignStatement(
                            fieldReference,
                            parameterIndexerExpression));
                }

                //TODO: constructor.Statements.Add for default values?
            }

            //TODO: Generate properties for each technique and pass

            this.ReportProgress(2, 3);

            // Generate source code
            CodeDomProvider codeProvider = this.GetCodeProvider();
            StringWriter writer = new StringWriter();
            codeProvider.GenerateCodeFromNamespace(codeNamespace, writer, null);
            
            #endregion

            this.ReportProgress(3, 3);

            return writer.ToString();
        }

		protected override byte[] GenerateCode(string contents)
        {
            try
            {
                return System.Text.Encoding.ASCII.GetBytes(GenerateCodeForFile());
            }
            catch (GenerationFailedException)
            {
                // For GenerationedFailedExceptions, one or more errors should
                // have been logged using GeneratorError, so do notihng here.
            }
            catch (Exception e)
            {
                // Some unexpected error has occured; log what we know about it.
                this.GeneratorError(4, Strings.UnexpectedException + e.ToString(), LineOrColumnNull, LineOrColumnNull);
            }
            
            // Returning null signifies that an error has occured.
            return null;
        }

        /*
         * for debugging
        static void Main(String[] args)
        {
            EffectGenerator eg = new EffectGenerator();
            eg.GenerateCodeForFile();
        }*/
    }
}

